Skip to content

Conversation

oxkitsune
Copy link
Contributor

@oxkitsune oxkitsune commented Aug 24, 2022

Implement BugPattern to ensure JUnit test classes are declared as package private final.

Example:

-public class A {
-  @Test
-  void foo () {}
-}

+final class A {
+  @Test
+  void foo () {}
+}

Suggested commit message:

Introduce `JUnitClassModifiers` check (#214)

@oxkitsune oxkitsune requested review from Stephan202 and rickie August 24, 2022 08:09
@oxkitsune oxkitsune marked this pull request as draft August 25, 2022 09:24
@oxkitsune oxkitsune marked this pull request as ready for review October 21, 2022 08:57
Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a commit.

We need to think about nested classes. What if that occurs, what are the rules w.r.t. modifiers. Additionally, we need a test case for that 😉.

Maybe we could to a test run on an internal codebase to see how it performs 😄.

@rickie rickie added this to the 0.5.0 milestone Oct 24, 2022
Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a commit.

Also asked a few questions :).

"",
"// BUG: Diagnostic contains:",
"class B {",
" @ParameterizedTest",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a specific reason to use @ParameterizedTest? To also account for that case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we check whether the class contains methods annotated with @Test or with meta annotation org.junit.jupiter.api.TestTemplate. @ParameterizedTest is annotated with @TestTemplate and this test ensures we also match tests like this.

@rickie rickie modified the milestones: 0.5.0, 0.6.0 Oct 30, 2022
@oxkitsune oxkitsune changed the title Introduce JUnitClassDeclaration Introduce JUnitClassModifiers check Nov 1, 2022
@oxkitsune oxkitsune requested a review from rickie November 1, 2022 09:31
Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a commit.

I think this is a really nice improvement. Would love to see this applied on our internal codebase :).

I found some edge cases that we should consider:

  • If something is abstract, it cannot be final. So we need to exclude that and add a test case for that :).
  • In our shared modules we have some public abstract classes with tests that can be used by downstream projects. These classes are not located in /src/test/ and marked public on purpose. I think this use case is rather specific to our shared modules repository, but IMO we should do something with it. On the other hand, I'm not sure if looking at the file's location is something we should add as heuristic for flagging these things.

tags = FRAGILE_CODE)
public final class JUnitClassModifiers extends BugChecker implements ClassTreeMatcher {
private static final long serialVersionUID = 1L;
private static final MultiMatcher<MethodTree, AnnotationTree> TEST_METHOD =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are re-using some of those, should we maybe move them to MoreMatchers? I discussed this with @eric-picnic already and he made a setup in #319. Although there we have a specific JUnit util.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can for now roll with this and have the bigger discussion in #319 as we have some more things that we plan to extract to utils there. So I'd say let's keep this here for now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a https://github.com/google/error-prone/blob/master/check_api/src/main/java/com/google/errorprone/matchers/JUnitMatchers.java that sadly enough only supports JUnit lower than 5... Ideally we contribute improvements upstream, but I'm not sure that will work...

Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thought :).

@oxkitsune
Copy link
Contributor Author

Our MoreMatchers#hasMetaAnnotation(String) method cannot match Spring's @Configuration as meta annotation.
I suspect this has something to do with the fact that @Configuration is not explicitly @Inherited.
I've implemented a quick work around that flags some often used @Configuration annotations for now.

Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted the introduction of MoreMatchers as this will be part of #335.

This means we'll temporarily introduce two duplicate methods hasMetaAnnotation and hasAnnotation. However, they will soon be de-duplicated by #335.
I seems better to focus this PR solely on introducing the JUnitClassModifiers check.

Suggested commit message (repeating PR title):

Introduce `JUnitClassModifiers` check (#214)

Really cool check @oxkitsune , this will have many impact on other teams 🚀 !

allOf(
not(hasMetaAnnotation("org.springframework.context.annotation.Configuration")),
not(
hasMetaAnnotation(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can combine these three not(hasMetaAnnotation( by wrapping them in a not(anyOf(.

Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One open question from my side left.

linkType = CUSTOM,
link = BUG_PATTERNS_BASE_URL + "JUnitClassDeclaration",
severity = WARNING,
tags = FRAGILE_CODE)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
tags = FRAGILE_CODE)
tags = SIMPLIFICATION)

Maybe this would fit better? Or STYLE?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushing something as it certainly does not match the description of FRAGILE_CODE.

@oxkitsune oxkitsune force-pushed the gdejong/PSM-1571 branch 2 times, most recently from 9f1e0be to a512faa Compare November 22, 2022 12:43
Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving the latest changes.

Have one thing to ponder, in RefasterRuleModifiersTest we give more descriptive names to the classes. This makes it easier to see what exact case is being tested. We could decide to do the same here. Already liking the existing setup, so approving regardless.

Copy link
Member

@Stephan202 Stephan202 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased and added a commit. Only open point from my side is the @Configuration case. Feel free to send me example classes over Slack.

Updated the suggested commit message.

Comment on lines 46 to 51
not(
annotations(
AT_LEAST_ONE,
anyOf(
isType("org.springframework.context.annotation.Configuration"),
hasMetaAnnotation("org.springframework.context.annotation.Configuration")))),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we document for which specific cases we need this? (And does this perhaps only prevent the annotated class from being declared final?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any springboot tests using an annotation marked with @Configuration cannot be marked final. Usually these classes can be package private, so I'm in favour of doing that.

Comment on lines 50 to 51
" @Nested",
" // BUG: Diagnostic contains:",
" class Nested1 {",
" @Test",
" void foo() {}",
" }",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @Nested doesn't influence the checker one way or another 👀

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought in this case it could be seen as "more realistic"? As the docs of this annotation mentions that it can be used in these kind of cases. OTOH it could also be deleted indeed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that realism is better, but (IIUC) there's no code in the checker that could possibly be influenced by the annotation. I'm not sure where exactly we should draw the line, though simpler seems preferred 🤔

@BugPattern(
summary = "JUnit test classes should be declared as package private final",
linkType = CUSTOM,
link = BUG_PATTERNS_BASE_URL + "JUnitClassDeclaration",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
link = BUG_PATTERNS_BASE_URL + "JUnitClassDeclaration",
link = BUG_PATTERNS_BASE_URL + "JUnitClassModifiers",

@github-actions
Copy link

github-actions bot commented Dec 1, 2022

Looks good. All 8 mutations in this change were killed.

class surviving killed
🎉tech.picnic.errorprone.bugpatterns.JUnitClassModifiers 0 8

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

1 similar comment
@github-actions
Copy link

github-actions bot commented Dec 1, 2022

Looks good. All 8 mutations in this change were killed.

class surviving killed
🎉tech.picnic.errorprone.bugpatterns.JUnitClassModifiers 0 8

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

@github-actions
Copy link

github-actions bot commented Dec 1, 2022

Looks good. All 9 mutations in this change were killed.

class surviving killed
🎉tech.picnic.errorprone.bugpatterns.JUnitClassModifiers 0 9

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

Copy link
Member

@rickie rickie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's get this one out 😄.

return Description.NO_MATCH;
}

boolean hasSpringConfiguration = TEST_CLASS_WITH_SPRING_CONFIGURATION.matches(tree, state);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the code. Got one minor point to consider, do we want to name it TEST_CLASS_WITH_SPRING_CONFIGURATION_ANNOTATION? It's a bit long I'd say but WITH_SPRING_CONFIGURATION out of context is not that clear.

Copy link
Member

@Stephan202 Stephan202 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased and added one more commit. Repeating the suggested commit message:

Introduce `JUnitClassModifiers` check (#214)

Comment on lines 73 to 76
boolean hasSpringConfiguration = TEST_CLASS_WITH_SPRING_CONFIGURATION.matches(tree, state);
if (hasSpringConfiguration && tree.getModifiers().getFlags().isEmpty()) {
return Description.NO_MATCH;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this test can be pushed into the TEST_CLASS_WITH_INCORRECT_MODIFIERS matcher; will push a proposal. (Downside is that the matcher would be evaluated twice in case of a hit, but that should rarely happen, since we expect users to clean up their code ;). Plus, it's not an expensive check.)

hasModifier(Modifier.PROTECTED),
hasModifier(Modifier.PUBLIC)));

private static final MultiMatcher<ClassTree, AnnotationTree>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private static final MultiMatcher<ClassTree, AnnotationTree>
private static final Matcher<ClassTree>

Would also omit the empty line above.

private static final Matcher<ClassTree> TEST_CLASS_WITH_INCORRECT_MODIFIERS =
allOf(
hasMethod(TEST_METHOD),
not(hasModifier(Modifier.ABSTRACT)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this case down so that the modifier ordering here matches the resolution order below.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^ I'll shuffle the test code accordingly.

Comment on lines 85 to 78
if (!hasSpringConfiguration) {
SuggestedFixes.addModifiers(tree, state, Modifier.FINAL).ifPresent(fixBuilder::merge);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pitest doesn't flag it because we're not using the STRONGER mutators group (I'll fix that), but right now the tests also pass if this suggested fix is applied unconditionally. We need to extend the replacement test as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll fix that

I filed #383.

@github-actions
Copy link

github-actions bot commented Dec 3, 2022

Looks good. All 6 mutations in this change were killed.

class surviving killed
🎉tech.picnic.errorprone.bugpatterns.JUnitClassModifiers 0 6

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

Copy link
Member

@werli werli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule itself LGTM, but I have one more question on one more of the test cases.

I'm optimistically approving however.

Comment on lines +65 to +75
" // BUG: Diagnostic contains:",
" class NonFinal {",
" @Test",
" void foo() {}",
" }",
"",
" // BUG: Diagnostic contains:",
" class NonFinalWithCustomTestMethod {",
" @ParameterizedTest",
" void foo() {}",
" }",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like these two cases are misleading: IIUC, the latter doesn't fail due to the "custom test method" but solely because it's non-final. Did I get this right?

Shouldn't we add a positive test case for test methods annotated with @ParameterizedTest? Though one could argue this is already covered by the test cases for MoreJUnitMatchers#TEST_METHOD in MoreJUnitMatchersTest.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did I get this right?

Yes :)

Shouldn't we add a positive test case for test methods annotated with @ParameterizedTest?

Can do :)

Copy link
Member

@Stephan202 Stephan202 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tnx for the review @werli! Added one more commit.

Comment on lines +65 to +75
" // BUG: Diagnostic contains:",
" class NonFinal {",
" @Test",
" void foo() {}",
" }",
"",
" // BUG: Diagnostic contains:",
" class NonFinalWithCustomTestMethod {",
" @ParameterizedTest",
" void foo() {}",
" }",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did I get this right?

Yes :)

Shouldn't we add a positive test case for test methods annotated with @ParameterizedTest?

Can do :)

@github-actions
Copy link

github-actions bot commented Dec 4, 2022

Looks good. All 6 mutations in this change were killed.

class surviving killed
🎉tech.picnic.errorprone.bugpatterns.JUnitClassModifiers 0 6

Mutation testing report by Pitest. Review any surviving mutants by inspecting the line comments under Files changed.

@rickie
Copy link
Member

rickie commented Dec 4, 2022

Nice reviews and commits 🚀 !

@rickie rickie merged commit 8803d23 into master Dec 4, 2022
@rickie rickie deleted the gdejong/PSM-1571 branch December 4, 2022 15:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

4 participants